package fr.vvins.core.search;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServer;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
import org.apache.solr.client.solrj.impl.CommonsHttpSolrServer;
import org.apache.solr.client.solrj.impl.XMLResponseParser;
import org.apache.solr.client.solrj.request.QueryRequest;
import org.apache.solr.client.solrj.response.FacetField;
import org.apache.solr.client.solrj.response.FacetField.Count;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.response.RangeFacet;
import org.apache.solr.client.solrj.response.SolrPingResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.servlet.SolrRequestParsers;
import org.hibernate.service.spi.ServiceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;

import fr.vvins.core.dao.Entity;
import fr.vvins.core.search.query.SearchFacetResult;
import fr.vvins.core.search.query.SearchQuery;
import fr.vvins.core.search.query.SearchResult;
import fr.vvins.core.search.query.SuggestQuery;


/**
 * This manager allow the connection to Solr search engine.
 * It's a schedule manager because it must allow to refresh the 
 * search index.
 * There are 2 differents type of configuration : 
 * - The first one 'embbeded' must be use on the instance where solr is installed
 * This instance should be in charge of index rebuilding. However it's possible
 * to launch rebuild from remote server.
 * => put localServer=true to use this configuration and set solrHome.
 * - The second on 'remote' use common http solrj interface. It use http connection
 * to connect to solr instance.
 * => put localServer=false to use this configuration and set solrHome.
 * @author fguay
 */
public class SearchEngine {

    private static final String RANGE_GAP = "gap";
	private static final String RANGE_END = "end";
	private static final String RANGE_START = "start";
	private static final String TERMS_PATH = "/terms";
    private static final String PARAMS_TERMS_FIELD = "terms.fl";
    private static final String PARAMS_TERMS = "terms";
    private static final String PARAMS_TERMS_LIMIT = "terms.limit";
    private static final String PARAMS_TERMS_LOWER_INCL = "terms.lower.incl";
    private static final String PARAMS_TERMS_PREFIX = "terms.prefix";
    private static final String PARAMS_TERMS_LOWER = "terms.lower";
    private static final String INDEX_PATH = "/dataimport";
    private static final String INDEX_METHOD_FULL = "full-import";
    private static final String INDEX_COMMAND = "command";
    private static final String INDEX_ITEM_DESCRIPTOR = "entityClass";
    private static final String INDEX_ID = "id";
    private static final String SYS_SOLR_HOME = "solr.solr.home";

    private SolrServer server;
    private boolean localServer;
    private String remoteServerUrl;
    private File solrHome;
    private int soTimeout;
    private int connectionTimeout;
    private int defaultMaxConnectionsPerHost;
    private int maxTotalConnections;
    private boolean followRedirects;
    private boolean allowCompression;
    private int maxRetries;
    private boolean useXmlParser;
    @PersistenceContext
    private EntityManager entityManager;
    
	private final static Logger log = LoggerFactory.getLogger(SearchEngine.class);

    
    /**
     * A tools method to check the component configuration.
     * @throws ServiceException
     */
    public void checkconfig() throws SearchEngineException {
        if(isLocalServer() && (solrHome == null || !solrHome.exists() || !solrHome.isDirectory())){
            throw new SearchEngineException("SearchEngineManager is not correctly configured. " +
            		"Please set correctly solrHome if you want to use local mode.");
        } else if (!isLocalServer() && (getRemoteServerUrl() == null)){
            throw new SearchEngineException("SearchEngineManager is not correctly configured. " +
            "Please set correctly solrUrl if you want to use remote mode.");
        }
    }

    public void init() throws SearchEngineException {
        checkconfig();
        //Always set the property as the webapp always starts and requires it.
        //System.setProperty(SYS_SOLR_HOME, getSolrHome().getAbsolutePath());
       
        if (isLocalServer()) {
            try {
                CoreContainer.Initializer initializer = new CoreContainer.Initializer();
                CoreContainer coreContainer = initializer.initialize();
                setServer(new EmbeddedSolrServer(coreContainer, ""));
            } catch (IOException e) {
                if (log.isErrorEnabled()) {
                    log.error("doStartService - Error : " + e.getMessage());
                }
                throw new SearchEngineException("Error during search engine start in embedded mode : " + e.getMessage());
            } catch (ParserConfigurationException e) {
                if (log.isErrorEnabled()) {
                    log.error("doStartService - Error : " + e.getMessage());
                }
                throw new SearchEngineException("Error during search engine start in embedded mode : " + e.getMessage());
            } catch (SAXException e) {
                if (log.isErrorEnabled()) {
                    log.error("doStartService - Error : " + e.getMessage());
                }
                throw new SearchEngineException("Error during search engine start in embedded mode : " + e.getMessage());
            }
        } else {
            try {
                CommonsHttpSolrServer serverHttp = new CommonsHttpSolrServer(getRemoteServerUrl());
                serverHttp.setSoTimeout(getSoTimeout()); // socket read timeout
                serverHttp.setConnectionTimeout(getConnectionTimeout());
                serverHttp.setDefaultMaxConnectionsPerHost(getDefaultMaxConnectionsPerHost());
                serverHttp.setMaxTotalConnections(getMaxTotalConnections());
                serverHttp.setFollowRedirects(isFollowRedirects()); // defaults
                                                                   // to false
                serverHttp.setAllowCompression(isAllowCompression());
               
                serverHttp.setMaxRetries(getMaxRetries()); // defaults to 0. > 1
                                                           // not recommended.
                if(isUseXmlParser()){
                	serverHttp.setParser(new XMLResponseParser()); // binary parser
                }                                               // is used by
                // default
                setServer(serverHttp);
            } catch (MalformedURLException e) {
                if (log.isErrorEnabled()) {
                    log.error("doStartService - Error : " + e.getMessage());
                }
                throw new SearchEngineException("Error during search engine start. " + "The URL : " + getRemoteServerUrl() + " is not correctly configured : " + " : " + e.getMessage());
            }
        }
        //if(log.isInfoEnabled()){
            //log.info("Test solr connection : " + ping());
        //}
    }
    
    /*protected synchronized void startScheduledJob() {
		if(!isLocalServer()){
	       if(log.isDebugEnabled()){
	      	 log.debug("Re build index mustn't manage on remonte search server. Change configuration of SearchEngineManager.");
	       }
	    }
		if(log.isInfoEnabled()){
			log.info("Register schedule job for re-index...");
		}
		super.startScheduledJob();
    }
*/
    
    /**
     * Return a list of term available in search engine.
     * @param pQuery
     * @return
     * @throws SearchEngineException
     */
    @SuppressWarnings("unchecked")
    public List<String> suggest(SuggestQuery pQuery) throws SearchEngineException {
    	if(log.isDebugEnabled()){
    		log.debug(pQuery.toString());
    	}
        List<String> autocomplete =null;
        String term = pQuery.getTerm().toLowerCase().trim();
        ModifiableSolrParams params = new ModifiableSolrParams();
        params.set(PARAMS_TERMS, true);
        String[] fields = new String[pQuery.getFields().size()];    
        pQuery.getFields().toArray(fields);
        params.set(PARAMS_TERMS_FIELD, fields);
        params.set(PARAMS_TERMS_LOWER, term);
        params.set(PARAMS_TERMS_PREFIX, term);
        params.set(PARAMS_TERMS_LOWER_INCL, false);
        params.set(PARAMS_TERMS_LIMIT, pQuery.getLimit());
        
        QueryRequest autoCompleteRequest = new QueryRequest(params);
        autoCompleteRequest.setPath(TERMS_PATH);
        
        try {
            QueryResponse resp = autoCompleteRequest.process(getServer());
            NamedList<Object> termsInfo = resp.getResponse();
            autocomplete = new ArrayList<String>();
            if(termsInfo.size() > 0){
	            NamedList<Object> items = (NamedList<Object>)((NamedList<Object>) termsInfo.getVal(1)).getVal(0);
	            for (int j = 0; j < items.size(); j++) {
	                autocomplete.add(items.getName(j));
	            }
	            return autocomplete;
            }
        } catch (SolrServerException e) {
            if(log.isErrorEnabled()){
                log.error("autocomplete - Error : " + e.getMessage());
            }
            throw new SearchEngineException(e.getMessage());
        }
        return null;
    }
    
    
    /**
     * Execute a query on solr server.
     * @param queryString : a query string on solr index fields
     * @param start : pagination tools : start row
     * @param max  : pagination tools : max row index
     * @param sortParams :  sort configuration.
     * @return
     * @throws SearchEngineException
     */
    public SearchResult executeQuery(String pQuery) throws SearchEngineException {
    	if(log.isDebugEnabled()){
    		log.debug(pQuery.toString());
    	}
        SearchResult results = null;
        SolrParams query = SolrRequestParsers.parseQueryString(pQuery);
        try {
            QueryResponse rsp = getServer().query(query);
            results = new SearchResult();
            results.setElapsedTime(rsp.getElapsedTime());
            results.setqTime(rsp.getQTime());
            results.setStatus(rsp.getStatus());
            SolrDocumentList docs = rsp.getResults();
            results.setStart(docs.getStart());
            results.setFound(docs.getNumFound());
            if (docs != null && !docs.isEmpty()) {
                for (Iterator<SolrDocument> it = docs.iterator(); it.hasNext();) {
                    SolrDocument doc = it.next();
                    Entity itemToAdd = resolveItem(doc);
                    if (itemToAdd != null) {
                        results.addResult(itemToAdd);
                    }
                }
            }
            return results;
        } catch (SolrServerException e) {
            if (log.isErrorEnabled()) {
                log.error("executeQuery - Error: ", e);
            }
            throw new SearchEngineException(e.getMessage(), e);
        }
    }
    


    /**
     * Execute a query on solr server.
     * @param queryString : a query string on solr index fields
     * @param start : pagination tools : start row
     * @param max  : pagination tools : max row index
     * @param sortParams :  sort configuration.
     * @return
     * @throws SearchEngineException
     */
    public SearchResult executeQuery(SearchQuery pQuery) throws SearchEngineException {
    	if(log.isDebugEnabled()){
    		log.debug(pQuery.toString());
    	}
        SearchResult results = null;
        SolrQuery query = new SolrQuery();
        query.setQuery(pQuery.getQuery());
        query.setFacet(pQuery.getFacet());
    	if(pQuery.getFacetFields()!= null){
	        for(String ff : pQuery.getFacetFields()){
	    		query.addFacetField(ff);
	    	}
    	}
    	if(pQuery.getFacetQuery() != null){
    		query.addFacetQuery(pQuery.getFacetQuery());
    	}
        
        if(pQuery.getFilterQuery() != null) {
        	String[] filterQueries = new String[pQuery.getFilterQuery().size()];
        	pQuery.getFilterQuery().toArray(filterQueries);
        	query.setFilterQueries(filterQueries);
        }
        if(pQuery.getRangeLimit() != null){
        	for(String rfield : pQuery.getRangeLimit().keySet()){
        		Map<String,Long> fieldRangeLimit = pQuery.getRangeLimit().get(rfield);
        		query.addNumericRangeFacet(rfield, fieldRangeLimit.get(RANGE_START), fieldRangeLimit.get(RANGE_END), fieldRangeLimit.get(RANGE_GAP));
        	}
        }
        if(pQuery.getStart() != -1) {
            query.setStart(pQuery.getStart());
        }
        if(pQuery.getMax() != -1) {
            query.setRows(pQuery.getMax());
        }
        if (pQuery.getSortParams() != null) {
            for (String field : pQuery.getSortParams().keySet()) {
                query.addSortField(field, SolrQuery.ORDER.valueOf(pQuery.getSortParams().get(field).toString()));
            }
        }
        try {
            QueryResponse rsp = getServer().query(query);
            results = new SearchResult();
            results.setElapsedTime(rsp.getElapsedTime());
            results.setqTime(rsp.getQTime());
            results.setStatus(rsp.getStatus());
            SolrDocumentList docs = rsp.getResults();
            results.setStart(docs.getStart());
            results.setFound(docs.getNumFound());
            if (docs != null && !docs.isEmpty()) {
                for (Iterator<SolrDocument> it = docs.iterator(); it.hasNext();) {
                    SolrDocument doc = it.next();
                    Entity itemToAdd = resolveItem(doc);
                    if (itemToAdd != null) {
                        results.addResult(itemToAdd);
                    }
                }
            }
            return results;
        } catch (SolrServerException e) {
            if (log.isErrorEnabled()) {
                log.error("executeQuery - Error: ", e);
            }
            throw new SearchEngineException(e.getMessage(), e);
        }
    }
    
    /**
     * Execute a query on solr server.
     * @param queryString : a query string on solr index fields
     * @param start : pagination tools : start row
     * @param max  : pagination tools : max row index
     * @param sortParams :  sort configuration.
     * @return
     * @throws SearchEngineException
     */
    public SearchFacetResult executeFacetQuery(SearchQuery pQuery) throws SearchEngineException {
    	
    	if(log.isDebugEnabled()){
    		log.debug(pQuery.toString());
    	}
    	SearchFacetResult results = new SearchFacetResult();
    	QueryResponse rsp = executeFacetsCount(pQuery);
    	results.setElapsedTime(rsp.getElapsedTime());
        results.setqTime(rsp.getQTime());
        results.setStatus(rsp.getStatus());
        SolrDocumentList docs = rsp.getResults();
        results.setStart(docs.getStart());
        results.setFound(docs.getNumFound());
        
        if (docs != null && !docs.isEmpty()) {
            for (Iterator<SolrDocument> it = docs.iterator(); it.hasNext();) {
                SolrDocument doc = it.next();
                Entity itemToAdd = resolveItem(doc);
                if (itemToAdd != null) {
                    results.addResult(itemToAdd);
                }
            }
        }
        
        Collection<FacetField> fields = rsp.getFacetFields();
    	Collection<String> facetLimit = pQuery.getFacetLimit();
    	List<RangeFacet> rangeFields = rsp.getFacetRanges();
    	if(fields != null && fields.size() != 0){
    		for(FacetField field : fields){
    			if(field.getValueCount() > 0){
    				List<Count> counts = field.getValues();
    				for(Count count : counts){
    					if(facetLimit == null || (facetLimit != null && facetLimit.contains(count.getName()))){
    						results.addFacetScore(field.getName(),count.getName(), count.getCount());
    					} 
    				}
    			}
    		}
    	}
    	if(rangeFields != null && rangeFields.size() != 0){
    		for(RangeFacet field : rangeFields){
    			if(field.getCounts() != null && field.getCounts().size() != 0){
    				results.clearFacetScore(field.getName());
    				List<RangeFacet.Count> counts = field.getCounts();
    				for(RangeFacet.Count count : counts){
    					results.addFacetScore(field.getName(),count.getValue(), count.getCount());
    				}
    			}
    		}
    	}
    	return results;
    }
    
    

	/**
     * Execute a facet search on specific Query.
     * 
     * @param pQuery
     * @return
     * @throws SearchEngineException
     */
    protected QueryResponse executeFacetsCount(SearchQuery pQuery) throws SearchEngineException {

        SolrQuery query = new SolrQuery();
        if(pQuery.getQuery() != null){
        	query.setQuery(pQuery.getQuery());	
        } else {
        	query.setQuery("*:*");	
        }
        query.setFacet(true);
        query.setFacet(pQuery.getFacet());
    	for(String ff : pQuery.getFacetFields()){
    		query.addFacetField(ff);
    	}
    	if(pQuery.getFacetQuery() != null){
    		query.addFacetQuery(pQuery.getFacetQuery());
    	}
    	if(pQuery.getFilterQuery() != null) {
        	String[] filterQueries = new String[pQuery.getFilterQuery().size()];
        	pQuery.getFilterQuery().toArray(filterQueries);
        	query.setFilterQueries(filterQueries);
        }
    	if(pQuery.getRangeLimit() != null){
        	for(String rfield : pQuery.getRangeLimit().keySet()){
        		Map<String,Long> fieldRangeLimit = pQuery.getRangeLimit().get(rfield);
        		query.addNumericRangeFacet(rfield, fieldRangeLimit.get(RANGE_START), fieldRangeLimit.get(RANGE_END), fieldRangeLimit.get(RANGE_GAP));
        	}
        }
        try {
        	
            QueryResponse rsp = getServer().query(query);
            return rsp;
        } catch (SolrServerException e) {
            if (log.isErrorEnabled()) {
                log.error("executeFacetsCount - Error: ", e);
            }
            throw new SearchEngineException(e.getMessage(), e);
        }
    }
   

    
    /**
     * This method allow to resolve a repositoryItem from a sold document return
     * by search engine.
     * 
     * @param pDoc
     * @return
     * @throws SearchEngineException
     */
    private Entity resolveItem(SolrDocument pDoc) throws SearchEngineException {
        String itemDescriptor = (String) pDoc.getFieldValue(INDEX_ITEM_DESCRIPTOR);
        String id = (String) pDoc.getFieldValue(INDEX_ID);
		try {
			Class<? extends Entity> entityClass = (Class<? extends Entity>) Class.forName(itemDescriptor);		
			return entityManager.find(entityClass, Integer.parseInt(id));
        } catch (Throwable e) {
            if (log.isErrorEnabled()) {
                log.error("resolveItem - Error : " + e.getMessage());
            }
            throw new SearchEngineException(e.getMessage(), e);
        }
    }

    /**
     * Rebuild completely search index. This method must be launch only on the
     * embbeded server.
     */
    public void rebuildIndex() {
        ModifiableSolrParams params = new ModifiableSolrParams();
        params.set(INDEX_COMMAND, INDEX_METHOD_FULL);
        QueryRequest fullindexRequest = new QueryRequest(params);
        fullindexRequest.setPath(INDEX_PATH);

        try {
            fullindexRequest.process(getServer());
        } catch (SolrServerException e) {
            if (log.isErrorEnabled()) {
                log.error("rebuildIndex - Error during rebuild process : " + e.getMessage(),e);
                log.error("Rebuild request : " + fullindexRequest.toString());
                log.error("Solr Path : " + getSolrHome());
                log.error("Server Info : " + getServer().toString());
                log.error("Ping :  " + ping());
            }
        }
    }

    /**
     * A test method... must be on junit test.
     */
    public void test() {
        try {
            SearchQuery query = new SearchQuery();
            query.setQuery("*:*");
            query.setMax(10);
            query.setStart(0);
            executeQuery(query);
            query.setFacet(true);
            executeQuery(query);
            executeFacetQuery(query);
            
        } catch (SearchEngineException e) {
            if (log.isErrorEnabled()) {
                log.error("test - Error : " + e.getMessage());
            }
        }
    }

    /**
     * Check the configuration of server and test the connection.
     * @return
     */
    public String ping() {
        try {
            SolrPingResponse rep = getServer().ping();
            if (log.isDebugEnabled()) {
                log.debug(rep.toString());
            }
            return rep.toString();
        } catch (SolrServerException e) {
            if (log.isErrorEnabled()) {
                log.error("ping - Error : " + e.getMessage());
            }
            return e.getMessage();
        } catch (IOException e) {
            if (log.isErrorEnabled()) {
                log.error("ping - Error : " + e.getMessage());
            }
            return e.getMessage();
        }
    }

    public boolean isLocalServer() {
        return localServer;
    }

    public void setLocalServer(boolean pLocalServer) {
        localServer = pLocalServer;
    }


    public File getSolrHome() {
        return solrHome;
    }

    public void setSolrHome(File pSolrHome) {
        solrHome = pSolrHome;
    }

    public SolrServer getServer() {
        return server;
    }

    public void setServer(SolrServer pServer) {
        server = pServer;
    }

    public int getSoTimeout() {
        return soTimeout;
    }

    public void setSoTimeout(int pSoTimeout) {
        soTimeout = pSoTimeout;
    }

    public int getConnectionTimeout() {
        return connectionTimeout;
    }

    public void setConnectionTimeout(int pConnectionTimeout) {
        connectionTimeout = pConnectionTimeout;
    }

    public int getDefaultMaxConnectionsPerHost() {
        return defaultMaxConnectionsPerHost;
    }

    public void setDefaultMaxConnectionsPerHost(int pDefaultMaxConnectionsPerHost) {
        defaultMaxConnectionsPerHost = pDefaultMaxConnectionsPerHost;
    }

    public int getMaxTotalConnections() {
        return maxTotalConnections;
    }

    public void setMaxTotalConnections(int pMaxTotalConnections) {
        maxTotalConnections = pMaxTotalConnections;
    }

    public boolean isFollowRedirects() {
        return followRedirects;
    }

    public void setFollowRedirects(boolean pFollowRedirects) {
        followRedirects = pFollowRedirects;
    }

    public boolean isAllowCompression() {
        return allowCompression;
    }

    public void setAllowCompression(boolean pAllowCompression) {
        allowCompression = pAllowCompression;
    }

    public int getMaxRetries() {
        return maxRetries;
    }

    public void setMaxRetries(int pMaxRetries) {
        maxRetries = pMaxRetries;
    }

    public String getRemoteServerUrl() {
        return remoteServerUrl;
    }

    public void setRemoteServerUrl(String pRemoteServerUrl) {
        remoteServerUrl = pRemoteServerUrl;
    }

	public boolean isUseXmlParser() {
		return useXmlParser;
	}

	public void setUseXmlParser(boolean useXmlParser) {
		this.useXmlParser = useXmlParser;
	}

}
